use super::*;
use std::collections::HashMap;

#[derive(Clone, Copy)]
pub struct Object<'val> {
	pub(crate) env: &'val Env,
	pub(crate) inner: sys::napi_value,
}

impl<'val> Object<'val> {
	/// Creates a new JavaScript object
	pub fn new<'env>(env: &'env Env) -> Result<'static, Object<'env>> {
		let mut ret = core::ptr::null_mut();
		unsafe { sys::napi_create_object(env.raw(), &mut ret) }.result("napi_create_object")?;
		Ok(Object { env, inner: ret })
	}

	/// Creates a JavaScript object with specified properties
	pub fn with_props<'env>(env: &'env Env, properties: &[PropertyDescriptor]) -> Result<'static, Object<'env>> {
		let mut ret = core::ptr::null_mut();
		unsafe { sys::napi_create_object_with_properties(env.raw(), &mut ret, properties.len(), properties.as_ptr() as _) }
			.result("napi_create_object_with_properties")?;
		Ok(Object { env, inner: ret })
	}

	/// Converts the object to a Value
	pub fn v(&'_ self) -> Value<'val> {
		Value::new(self.env, self.inner)
	}

	/// Creates an object from key-value pairs
	pub fn from_entries<'env, K, V>(env: &'env Env, entries: &[(K, V)]) -> Result<'static, Object<'env>>
	where
		K: AsRef<str>,
		V: IntoNapi + Clone,
	{
		let object = Self::new(env)?;
		for (key, value) in entries {
			let key_value = Value::of(env, key.as_ref())?;
			let value_value = value.clone().into_napi(env)?;
			object.v().set_property(key_value, value_value)?;
		}
		Ok(object)
	}

	/// Creates an object from a HashMap
	pub fn from_hashmap<'env, V>(env: &'env Env, map: &HashMap<String, V>) -> Result<'static, Object<'env>>
	where
		V: IntoNapi + Clone,
	{
		let object = Self::new(env)?;
		for (key, value) in map {
			let key_value = Value::of(env, key.as_str())?;
			let value_value = value.clone().into_napi(env)?;
			object.v().set_property(key_value, value_value)?;
		}
		Ok(object)
	}

	/// Checks if the object is empty (has no properties)
	pub fn is_empty(&'_ self) -> Result<'static, bool> {
		let count = self.name_count()?;
		Ok(count == 0)
	}

	/// Freezes the object, preventing new properties from being added and existing properties from being removed or changed
	pub fn freeze(&'_ self) -> Result<'static, ()> {
		unsafe { sys::napi_object_freeze(self.env.raw(), self.inner) }.result("napi_object_freeze")
	}

	/// Seals the object, preventing new properties from being added and marking all existing properties as non-configurable
	pub fn seal(&'_ self) -> Result<'static, ()> {
		unsafe { sys::napi_object_seal(self.env.raw(), self.inner) }.result("napi_object_seal")
	}

	/// Gets the number of properties in the object
	pub fn name_count(&'_ self) -> Result<'static, usize> {
		let mut ret = core::ptr::null_mut();
		unsafe { sys::napi_get_property_names(self.env.raw(), self.inner, &mut ret) }.result("napi_get_property_names")?;
		let names_array = Value::new(self.env, ret).to::<Array>()?;
		names_array.length()
	}

	/// Gets all property names of the object as a vector of strings
	pub fn names(&'_ self) -> Result<'static, Vec<String>> {
		let mut ret = core::ptr::null_mut();
		unsafe { sys::napi_get_property_names(self.env.raw(), self.inner, &mut ret) }.result("napi_get_property_names")?;
		let names_array = Value::new(self.env, ret).to::<Array>()?;
		let len = names_array.length()?;
		let mut result = Vec::with_capacity(len as usize);

		for i in 0..len {
			result.push(names_array.get(i)?);
		}

		Ok(result)
	}

	/// Gets all property values of the object as a vector
	pub fn values<V>(&'_ self) -> Result<'static, Vec<V>>
	where
		V: FromNapi<'val>,
	{
		let names = self.names()?;
		let mut result = Vec::with_capacity(names.len());

		for name in names {
			let key = Value::of(self.env, &name)?;
			let value = self.v().get_property(key)?;
			let typed_value: V = value.to()?;
			result.push(typed_value);
		}

		Ok(result)
	}

	/// Merges multiple objects into a new object
	pub fn assign<'env>(env: &'env Env, objects: &[&Object<'val>]) -> Result<'static, Object<'env>> {
		let result = Object::new(env)?;

		for object in objects {
			let names = object.names()?;
			for name in names {
				let key = Value::of(env, &name)?;
				let value = object.v().get_property(key)?;
				result.v().set_property(key, value)?;
			}
		}

		Ok(result)
	}

	/// Shallow clones an object
	pub fn clone_shallow(&'_ self) -> Result<'static, Object<'val>> {
		let result = Object::new(self.env)?;
		let names = self.names()?;

		for name in names {
			let key = Value::of(self.env, &name)?;
			let value = self.v().get_property(key)?;
			result.v().set_property(key, value)?;
		}

		Ok(result)
	}

	/// Converts the object to a HashMap
	pub fn to_hashmap<V>(&'_ self) -> Result<'static, HashMap<String, V>>
	where
		V: FromNapi<'val>,
	{
		let names = self.names()?;
		let mut result = HashMap::with_capacity(names.len());

		for name in names {
			let key = Value::of(self.env, &name)?;
			let value = self.v().get_property(key)?;
			let typed_value: V = value.to()?;
			result.insert(name, typed_value);
		}

		Ok(result)
	}

	/// Converts the object to a vector of key-value pairs
	pub fn to_entries<V>(&'_ self) -> Result<'static, Vec<(String, V)>>
	where
		V: FromNapi<'val>,
	{
		let names = self.names()?;
		let mut result = Vec::with_capacity(names.len());

		for name in names {
			let key = Value::of(self.env, &name)?;
			let value = self.v().get_property(key)?;
			let typed_value: V = value.to()?;
			result.push((name, typed_value));
		}

		Ok(result)
	}
}

impl<'val> FromNapi<'val> for Object<'val> {
	/// Converts a Value to an Object
	fn from_napi(v: Value<'val>) -> Result<'static, Object<'val>> {
		if !v.is_object()? {
			return Err(Status::invalid_arg("value is not Object type"));
		}
		Ok(Object {
			env: v.env,
			inner: v.inner,
		})
	}
}

impl<'val> IntoNapi for Object<'val> {
	/// Converts an Object to a Value
	fn into_napi<'env>(self, env: &'env Env) -> Result<'static, Value<'env>> {
		if self.env.raw() != env.raw() {
			return Err(Status::invalid_arg("self.env not eq arg env"));
		}
		Ok(Value::new(env, self.inner))
	}
}

/// Implements IntoNapi for HashMap
impl<V: IntoNapi + Clone> IntoNapi for HashMap<String, V> {
	fn into_napi<'env>(self, env: &'env Env) -> Result<'static, Value<'env>> {
		Object::from_hashmap(env, &self)?.into_napi(env)
	}
}

/// Implements IntoNapi for Vec<(String, V)>
impl<V: IntoNapi + Clone> IntoNapi for Vec<(String, V)> {
	fn into_napi<'env>(self, env: &'env Env) -> Result<'static, Value<'env>> {
		Object::from_entries(env, &self)?.into_napi(env)
	}
}
